#include <QDebug>
#include <QFileInfo>
#include <QLibrary>
#include <QDirIterator>
#include "Modules.h"
#include "BinaryModule.h"
#include "CramBuilder.h"
#include "RestoreVisitor.h"

#include "grammar/python3/PythonGrammar30.h"
#include "grammar/python3/PythonGrammar30TokenManager.h"

#include "Scopes/ModuleScope.h"

Modules* Modules::_instance = NULL;

Modules::Modules():
    _binary(NULL)
{
}

Modules::~Modules()
{
    if (_binary)
        delete _binary;
    qDeleteAll(_modulesCache);
    qDebug() << "Modules count" << _modulesCache.count();
}

Modules* Modules::getModules()
{
    if (!_instance)
        _instance = new Modules();
    return _instance;
}

ModuleScope* Modules::moduleByName(const QString& moduleName, const QString& relativePath, bool useCache)
{
    if (useCache){
        if (!_modulesCache.contains(moduleName)){
            ModuleScope* module = addModuleByName(moduleName, relativePath, useCache);
            _modulesCache[moduleName] = module;
            _modulesCache[moduleName]->buildScope();
            return module;
        }
    } else {
        return addModuleByName(moduleName, relativePath, useCache);
    }
    return NULL;
}

ModuleScope* Modules::moduleByFileName(const QString& fileName, bool useCache)
{
    QFileInfo info(fileName);
    PropMap prop;
    prop["path"] = fileName;
    prop["name"] = info.baseName();
    prop["relpath"] = info.absolutePath();
    ModuleScope* module = _moduleByFileName(prop, useCache);
    QFileInfo fi(fileName);
    _modulesCache[fi.baseName()] = module;
    _modulesCache[fi.baseName()]->buildScope();
    return module;
}

ModuleScope* Modules::_moduleByFileName(const PropMap& file, bool useCache)
{
    if (file["path"].startsWith("binary://")){
        if (_cacheMap.isCached(file["path"])){
            return _pyParse(_cacheMap.getCachedFile(file["path"]));
        }
    } else {
        QFileInfo fi(file["path"]);
        if (fi.isFile() && (fi.suffix() == "py" || fi.suffix() == "pyw"))
            return parsePythonFile(file, useCache);
        else if (fi.isFile() && (fi.suffix() == "zip" || fi.suffix() == "egg"))
            return importZipPythonPackage(fi);
        else if (fi.isDir() && fi.suffix() == "egg")
            return importEggPackage(fi);
        else if (fi.isDir() && fi.suffix() != "egg")
            return importPackage(fi);
        else if (fi.isFile() && (fi.suffix() == "so" || fi.suffix() == "pyd"))
            return importBinary(file);
        return importBinary(file);
    }
    return NULL;
}


void Modules::end()
{
    delete _instance;
}

void Modules::setImportPathes()
{
    _importPathes = binary()->importPathes();
    qDebug() << _importPathes;
}

BinaryModule* Modules::binary()
{
    if (!_binary)
        _binary = new BinaryModule();
    return _binary;
}

ModuleScope* Modules::addModuleByName(const QString& name, const QString& relativePath, bool useCache)
{
    PropMap fileName = fileNameByImport(name, relativePath);
    return _moduleByFileName(fileName, useCache);
}

PropMap Modules::fileNameByImport(const QString& name, const QString& relativePath)
{
    QFileInfo ret;
    if (name.startsWith(".")){
        // relative import
    } else {
        QString fileName = _cacheMap.findCachedByModuleName(name);
        if (!fileName.isEmpty()){
            PropMap out;
            out["path"] = fileName;
            out["name"] = name;
            out["relpath"] = relativePath;
            return out;
        }

        if (!_importPathes.length())
            setImportPathes();

        QStringList chunks = name.split(".");
        QString fname = chunks.takeFirst();
        ret = findInPathes(fname, _importPathes);

        if (ret.isDir()){
            foreach(QString fchnuhk, chunks) {
                QFileInfo path = findInPathes(fchnuhk, QStringList() << ret.absoluteFilePath());
                if (path.exists())
                    ret = path;
            }
        }
        if (ret.isDir()){
            ret = QFileInfo(ret.absoluteFilePath()+QDir::separator()+"__init__.py");
        }
    }
    PropMap out;
    out["path"] = ret.absoluteFilePath();
    out["name"] = name;
    out["relpath"] = relativePath;
    return out;
}

QFileInfo Modules::findInPathes(const QString name, const QStringList& pathes)
{
    foreach(QString path, _importPathes){
        QDirIterator iter(path, QDirIterator::NoIteratorFlags);
        while(iter.hasNext()){
            iter.next();
            QFileInfo info = iter.fileInfo();
            if (info.baseName() == name)
                return info;
        }
    }
    return QFileInfo();
}

ModuleScope* Modules::parsePythonFile(const PropMap& fi, bool useCache)
{
    /*QString parseFileName = fi.absoluteFilePath();
    bool cached = false;
    if (useCache && _cacheMap.isCached(fi)){
        cached = true;
        parseFileName = _cacheMap.getCachedFile(fi);
    }
    qDebug() << "parse" << parseFileName << "from" << fi.absoluteFilePath();
    ModuleScope * mod = _pyParse(parseFileName);
    if (mod){
        if (!cached && useCache){
            CramModule boot(fi.baseName());
            restoreBootStrap(mod->ast(), &boot);
            QFile out(_cacheMap.getCachedFile(fi.baseName()));
            if (out.open(QIODevice::WriteOnly)){
                out.write(boot.build().toUtf8());
            }
            _cacheMap.addToCache(fi, false);
        }
    }
    return mod;*/
    return NULL;
}

ModuleScope* Modules::_pyParse(const QString& fileName)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly))
        return NULL;

    QString test = file.readAll();
    file.close();

    CharStream stream(test, test.length(), 0, 0);
    PythonGrammar30TokenManager in(&stream);
    PythonGrammar30 grammar(&in);

    ModulePtr newRoot = grammar.file_input().dynamicCast<Module>();
    ModuleScope* module = new ModuleScope(newRoot, fileName);
    return module;
}

ModuleScope* Modules::importZipPythonPackage(const QFileInfo& fi)
{
    return NULL;
}

ModuleScope* Modules::importEggPackage(const QFileInfo& fi)
{
    return NULL;
}

ModuleScope* Modules::importPackage(const QFileInfo& fi)
{
    return NULL;
}


ModuleScope* Modules::importBinary(const PropMap& fi)
{
    if (_cacheMap.isCached(fi["name"])){
        return parsePythonFile(fi, true);
    }
    QString moduleName = fi.baseName();
    CramModule boot(moduleName);

    PyObject* mod = binary()->importModule(moduleName);
    processScope(mod, &boot);
    qDebug() << "build cache" << _cacheMap.getCachedFile(fi);
    QFile out(_cacheMap.getCachedFile(fi));
    if (out.open(QIODevice::WriteOnly)){
        out.write(boot.build().toUtf8());
        out.close();
    }
    binary()->free(mod);
    _cacheMap.addToCache(fi, fi["name"], true);
    return _pyParse(_cacheMap.getCachedFile(fi));
}

void Modules::processScope(PyObject* obj, CramBuilder* builder, bool onlyPublic)
{
    QMap<QString, PyObject*> info = binary()->dir(obj);
    foreach(QString key, info.keys()){
        if (!info[key])
            continue;

        QString type = binary()->typeName(info[key]);
        //qDebug() << key << type;
        if (type == "str"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            QString doc = binary()->attrString(obj, key);
            if (key == "__doc__" || doc.split("\n").length() > 1)
                *builder << builder->ident << key << "=\"\"\"" << doc << "\"\"\"\n";
            else
                *builder << builder->ident << key << "=\"" << doc << "\"\n";
            //builder->addGlobalStr(key, binary()->attrString(obj, key));
        } else if (type == "module"){
            if (key != "__builtins__" && key != "builtins")
                *builder << builder->ident << "import " << key;
            //builder->addImport(key);
        } else if (type == "type"){
            if (onlyPublic && key.startsWith("__"))
                continue;
            CramClass *cls = builder->addClass(key, binary()->getMro(info[key]), binary()->getDoc(info[key]));
            //cls->setBases(binary()->getMro(info[key]));
            //cls->__doc__ = binary()->getDoc(info[key]);
            processScope(info[key], cls, true);
        } else if (type == "function" || type == "method_descriptor" || type == "builtin_function_or_method"){
            builder->addFunction(key, binary()->funcArgs(info[key], key), binary()->getDoc(info[key]), binary()->funcReturn(info[key], key));
            //fnc->__doc__ = ;
            //fnc->setArgs(binary()->funcArgs(info[key], key));
        }
        //qDebug() << key;
    }
    binary()->free(info);
}

void Modules::restoreBootStrap(const SimpleNodePtr& root, CramBuilder* builder)
{
    RestoreVisitor visit(builder);
    visit.traverse(root);
}
